#!/bin/bash

# Simple test wrapper around radclient to allow automated UATs
#
# Author Arran Cudbard-Bell <a.cudbardb@freeradius.org>
# Copyright 2014-2015 Arran Cudbard-Bell <a.cudbardb@freeradius.org>
# Copyright 2015 The FreeRADIUS Project

# A POSIX variable
OPTIND=1         # Reset in case getopts has been used previously in the shell.

# Environmental variables
: ${TESTDIR=$(dirname $0)"/tests"}
: ${RADCLIENT='radclient'}
: ${FILTER_SUFFIX='_expected'}
: ${DICTPATH=$(dirname $0)/share}
PATH="$(dirname $0)/bin:${PATH}"

# Initialize our own variables
verbose=0
cluster=
role=
type=
parallel=40
retries=3
timeout=2
target='127.0.0.1'
secret='testing123'

# Some very basic logging functions
function ERROR
{
    echo "$@" 1>&2;
}

function INFO
{
    echo "$@"
}

function DEBUG
{
    if [ $verbose -gt 0 ]; then
        echo "$@"
    fi
}

function show_help
{
    echo $(basename $0)" [options] [-- <test_glob0> <test_glob1> <test_globN>]"
    echo "  -h                        Display this help message."
    echo "  -H <host>[:port]          Send test packets to specified host and port (defaults to 127.0.0.1)"
    echo "  -v                        Verbose mode."
    echo "  -p <number>               Run tests in parallel (defaults to 20)."
    echo "  -s <secret>               Shared secret."
    if [ ! -z "$role_types" ]; then
        echo "  -c <cluster>              Specify cluster type one of ($cluster_types)."
        echo "  -r <type>                 Specify server role one of ($role_types)."
        echo
        echo "Note: Test path globs are relative to ${TESTDIR}/<cluster>/<type>/"
    fi

    echo
    echo "For role based test file layout create test files under ${TESTDIR}/<cluster>/<type>"
    echo "Where <cluster> and <type> are substrings found in the FQDN of <host>."
    echo "For simplified test layout create test files under ${TESTDIR}"
    echo
    echo "The directory containing the tests should contains pairs of request files and filter files."
    echo "The request file name must contain 'test<num><num><num>."
    echo "The filter name must match the test name but with a '${FILTER_SUFFIX}' suffix."
    echo "For example:"
    echo "  ${TESTDIR}/test000_my_first_test"
    echo "  ${TESTDIR}/test000_my_first_test${FILTER_SUFFIX}"
    echo
    echo "The directory containing the tests may have multiple subdirectories to group the tests."
}

RADCLIENT=$(which "$RADCLIENT")
if [ ! -x "$RADCLIENT" ]; then
    ERROR "Can't find radclient binary, modify your \$PATH or set RADCLIENT"
    exit 64
fi

if [ ! -d "$TESTDIR" ]; then
    ERROR "Test dir $TESTDIR does not exist, create it or specify it with TESTDIR=<dir>"
    show_help
    exit 64
fi

# Definitions (build these dynamically by looking at the files under tests)
cluster_dirs=$(find "$TESTDIR/" -mindepth 1 -maxdepth 1 -type d)
cluster_types=$(echo $cluster_dirs | sed 's/\s/ /g')

role_types=
for i in $cluster_dirs; do
    for j in $(find "$TESTDIR/$(basename $i)/" -mindepth 1 -maxdepth 1 -type d); do
        role=$(basename "$j")
        if [ "$role_types" == '' ]; then
            role_types="$role"
        else
            role_types+="\n$role"
        fi
    done
done

if [ -z "$role_types" ]; then
    DEBUG "Using simple test file layout"
else
    DEBUG "Using role based test file layout"
    role_types=$(echo -e "$role_types" | sort | uniq)   # Remove duplicates
    role_types=$(echo $role_types | sed 's/\s/ /g')     # Change \n back to spaces
fi

while getopts "h?H:vc:r:s:p:" opt; do
    case "$opt" in
    h|\?)
        show_help
        exit 0
        ;;

    v)
        verbose=1
        ;;

    c)
        found=0
        for i in $cluster_types; do
            if [ "$i" == "$OPTARG" ]; then
                found=1
            fi
        done
        if [ $found -ne 1 ]; then
            ERROR "'$OPTARG' is not a valid cluster type"
            show_help
            exit 64
        fi
        cluster="$OPTARG"
        ;;

    r)
        found=0
        for i in $role_types; do
            if [ "$i" == "$OPTARG" ]; then
                found=1
            fi
        done
        if [ $found -ne 1 ]; then
            ERROR "'$OPTARG' is not a valid role type"
            show_help
            exit 64
        fi
        role="$OPTARG"
        ;;

    s)
        secret="$OPTARG"
        ;;

    p)
        if ! echo "$OPTARG" | grep -E '^[0-9]+$' > /dev/null; then
            ERROR "Non integer argument '$OPTARG' specified for -p"
            show_help
            exit 64
        fi
        parallel=$OPTARG
        ;;

    H)
        target="$OPTARG"
        ;;

    esac
done

shift $((OPTIND-1))

[ "$1" = "--" ] && shift
test_files=$@

#
#  Match keywords from the hostname to clusters or roles
#
if [ ! -z "$role_types" ]; then
    this_host=$(hostname -f)
    for tok in $(echo "$this_host" | sed 's/\./ /g'); do
        for key in ${cluster_types}; do
            if echo "$tok" | grep "$key" > /dev/null && [ "$cluster" = '' ]; then cluster="$key"; fi
        done
        for key in ${role_types}; do
            if echo "$tok" | grep "$key" > /dev/null && [ "$role" = '' ]; then role="$key"; fi
        done
    done

    if [ "$cluster" == '' ]; then
        ERROR "Couldn't determine the cluster $this_host belongs to";
        show_help
        exit 64;
    fi

    if [ "$role" == '' ]; then
        ERROR "Couldn't determine the role $this_host performs";
        show_help
        exit 64;
    fi

    test_path="${TESTDIR}/${cluster}/${role}"
#
#  Otherwise just use the tests in the test dir
#
else
    test_path="${TESTDIR}"
fi

if [ "$test_files" != '' ]; then
    tmp=
    for glob in $test_files; do
        # Filter out response files (makes wildcards easier), and expand the globs
        for file in $(find "${test_path}" -depth -path "*${glob}" -and -not -path "*${FILTER_SUFFIX}" -and '(' -type f -or -type l ')'); do
            tmp+="${file} "
        done
    done
    test_files="${tmp}"
else
    # Lexicographical, depth-first
    test_files=$(find "$test_path" -depth -path '*test[0-9][0-9][0-9]*' -and -not -path "*${FILTER_SUFFIX}" -and '(' -type f -or -type l ')')
    if [ "$test_files" == '' ]; then
        ERROR "No test files found in $test_path"
        exit 64;
    fi
    INFO "Executing"$(echo "$test_files" | wc -l)" test(s) from ${test_path}"
fi

#
#  Check if we got any test files
#
if [ "$test_files" == '' ]; then
    ERROR "No test files to process"
    exit 1
fi

#
#  Output which files were going to be using for testing
#
if [ $verbose -eq 0 ]; then
    INFO "Executing specified tests"
    INFO "Use -v to see full list"
else
    INFO "Executing specified tests:"
    for i in $test_files; do
        DEBUG "$i"
    done
fi

#
#  Figure out which tests we can munge into a single file which we can
#  use to parallelise testing
#
base=$(basename $0)
packets=$(mktemp -t "${base}XXX") || exit 1
filters=$(mktemp -t "${base}XXX") || exit 1

args=
file_args=
serial_file_args=
for i in $test_files; do
    if [ ! -f "$i" -a ! -L "$i" ]; then
        INFO "Skipping $i: not file"
        continue
    fi

    if [ ! -r "$i" ]; then
        INFO "Skipping $i: not readable (check permissions)"
        continue
    fi

    expected="${i}${FILTER_SUFFIX}"
    if [ ! -f "$expected" -a ! -L "$expected" ]; then
        DEBUG "$i cannot be parallelised: Can't find 'expected' file"
        file_args+=" -f \"$i\""
        continue
    fi

    if [ ! -r "$expected" ]; then
        INFO "$i cannot be parallelised: 'expected' file not readable"
        file_args+=" -f \"${i}:${expected}\""
        continue
    fi

    if head -n 1 "$i" | grep -i -E '^#\s*serial' > /dev/null; then
        DEBUG "$i marked as serial only"
        serial_file_args+=" -f \"${i}:${expected}\""
        continue
    fi

    # Else add it to the master test file
    printf '%s\n' "$(cat "$i")" >> "$packets"

    # Add the name of the file so it appears in radclient debug output
    # and can later be specified with -v -- <test> to drill down.
    echo "Radclient-Test-Name := \""$(echo "$i" | sed -e "s@${test_path}/\?@@")"\"" >> "$packets"
    echo >> "$packets"
    printf '%s\n' "$(cat "${i}_expected")" >> "$filters"
    echo >> "$filters"
done

if [ `cat "$packets" | wc -l` -gt 0 ]; then
    file_args+=" -f \"${packets}:${filters}\""
fi

if [ ! -z "$file_args" ]; then
	args="$file_args"

	if [ $verbose -ne 0 ]; then
    		args+=" -x"
	fi

	args+=" -s"
	args+=" -t \"$timeout\""
	args+=" -r \"$retries\""
	args+=" -p \"$parallel\""
	args+=" -D \"$DICTPATH\""
	args+=" \"$target\""
	args+=" auto"
	args+=" \"$secret\""

	DEBUG "Executing: $RADCLIENT $args"
	eval $RADCLIENT $args; ret=$?
	INFO "(Parallelised tests)"
	INFO ""

	rm -f "$packets" 2>&1 > /dev/null
	rm -f "$filters" 2>&1 > /dev/null

	if [ $ret -ne 0 ]; then
    		ERROR "One or more tests failed (radclient exited with $ret)"
    		exit $ret
	fi
fi

if [ ! -z "$serial_file_args" ]; then
	args="$serial_file_args"

	if [ $verbose -ne 0 ]; then
    		args+=" -x"
	fi

	args+=" -s"
	args+=" -t \"$timeout\""
	args+=" -r \"$retries\""
	args+=" -p 1"
	args+=" -D \"$DICTPATH\""
	args+=" \"$target\""
	args+=" auto"
	args+=" \"$secret\""

	DEBUG "Executing: $RADCLIENT $args"
	eval $RADCLIENT $args; ret=$?
	INFO "(Serialised tests)"

	if [ $ret -ne 0 ]; then
    		ERROR "One or more tests failed (radclient exited with $ret)"
    		exit $ret
	fi
fi

exit 0
